package co.unal.sim;

import org.apache.commons.math3.distribution.ExponentialDistribution;
import org.apache.commons.math3.distribution.PoissonDistribution;

import co.unal.sim.queque.Queue;
import co.unal.sim.queque.Service;

/**
 * Esta clase simula la operación de un sistema M/M/1
 * 
 * El objetivo es entender el funcionamiento de la simulación basada en eventos
 * para convertirlo en un simulador de un sistema M/M/2
 * 
 * */
public class MM1 {
	// Variables de Estado
	private int nextEventType;
	private int numCustDelayed;

	private int numDelaysRequired;
	private int numEvents;
	//private int serverStatusB;

	private double simTime;
	private double timeLastEvent;
	private double totalDelays;
	private double[] timeNextEvent;
	private double areaNumInQueue;
	private double areaServerStatus;
	private double meanInterarrival;
	private double meanServiceB;
	
	private Queue queue;
	private Service service;
	
	public MM1() {
		queue = new Queue("Cola 1", 1);
		service = new Service("Servicio", 0.9);
	}

	/**
	 * @param args
	 */
	public static void main(String[] args) {

		MM1 s = new MM1();
		s.setNumEvents(2);
		s.setMeanInterarrival(1);
		s.setMeanServiceB(0.5);
		s.setNumDelaysRequired(100);

		s.initialize();

		/*
		 * La condición de ejecución es que se sirvan 100 clientes, podría
		 * cambiar a un rango de tiempo específico o cualquier otra condición.
		 */
		while (s.getNumCustDelayed() < s.getNumDelaysRequired()) {
			// Determinar el siguiente evento
			s.timing();

			// Actualizar los estadísticos de interés
			s.updateTimeAvgStats();

			// Ejecutar acciones de acuerdo al siguiente evento
			switch (s.getNextEventType()) {
			case 1:
				s.arrival();
				break;

			case 2:
				s.departure();
				break;

			/*
			 * Los casos adicionales que corresponden a MM2 deberán ser
			 * agregados aquí?
			 */

			default:
				break;
			}
			System.out.println("**********************************");

		}

		// Llamar al generador de reportes para concluir la simulación
		s.report();

	}

	private void initialize() {
		System.out.println("Inicializando el simulador...");

		// Inicia el reloj de simulación
		simTime = 0;

		// Inicia las variables de estado
		service.setServerStatus(Service.IDLE);
		//serverStatusB = IDLE;
		queue.setNumInQueue(0);
		timeLastEvent = 0;

		numCustDelayed = 0;
		totalDelays = 0;
		areaNumInQueue = 0;
		areaServerStatus = 0;

		timeNextEvent = new double[numEvents + 1];

		/* Asegurar que el primer evento es una llegada */
		timeNextEvent[1] = simTime + exp(meanInterarrival); // llegadas
		timeNextEvent[2] = Double.MAX_VALUE; // salidas

	}

	/*
	 * Determina el tipo del siguiente evento
	 */
	private void timing() {
		System.out.println("Ejecutando rutina de tiempo...");
		int i;
		double minTimeNextEvent = Double.MAX_VALUE;

		nextEventType = 0;

		/*
		 * Determinar el tipo del siguiente evento que va a ocurrir
		 */

		for (i = 1; i <= numEvents; ++i) {
			if (timeNextEvent[i] < minTimeNextEvent) {
				minTimeNextEvent = timeNextEvent[i];
				nextEventType = i;
			}

		}

		System.out.println("Tipo de siguiente evento: " + nextEventType);

		if (nextEventType == 0) {
			System.out.println("La lista de eventos está vacía.");
			System.out.println("Tiempo de simulación:" + simTime);
			System.exit(0);
		}

		simTime = minTimeNextEvent;
	}

	/*
	 * Evento de llegada de un cliente
	 */
	private void arrival() {
		System.out.println("Ejecutando rutina de llegada en: " + simTime);
		System.out.println(
				"Clientes en la cola " + queue.getName() + ": " 
						+ queue.getNumInQueue());
		double delay;

		/* Programar la siguiente llegada */

		timeNextEvent[1] = simTime + exp(meanInterarrival);

		/* Verificar si el servidor está ocupado */

		if (service.getServerStatus() == Service.BUSY) {
			/* Incrementar el número de clientes en la cola */
			queue.incrementQueue();

			/* Demasiados clientes en la fila */
			if (queue.getNumInQueue() > Queue.QUEUE_LIMIT) {
				System.out.println("Demasiados clientes en la fila.");
				System.out.println("Tiempo: " + simTime);
				System.exit(0);
			}

			queue.addArrivalTimes(simTime);

		} else {
			/* El servidor está disponible */
			delay = 0;
			totalDelays += delay;

			++numCustDelayed;
			service.setServerStatus(Service.BUSY);
			/* Programar el evento de salida del cliente */
			timeNextEvent[2] = simTime + exp(service.getMeanService());
		}

	}

	/* Función de evento de salida del cliente */
	private void departure() {
		System.out.println("Ejecutando rutina de salida en: " + simTime);
		System.out.println("Clientes en la cola: " + queue.getNumInQueue());
		double delay;

		/* Revisar si la cola está vacía */
		if (queue.getNumInQueue() == 0) {
			/*
			 * Cambiar estado del servidor y eliminar el evento departure de la
			 * lista de eventos
			 */
			service.setServerStatus(Service.IDLE);
			timeNextEvent[2] = Double.MAX_VALUE;

		} else {
			/*
			 * La fila no está vacía así que se debe descontar un cliente de
			 * ella
			 */
			queue.decrementQueue();

			/*
			 * Calcular la demora del cliente que empieza el servicio y
			 * actualice el contador total delays
			 */
			delay = simTime + queue.getArrivalTimes()[1];
			totalDelays += delay;

			/*
			 * Incrementar el número de clientes demorados y programar una
			 * salida
			 */
			++numCustDelayed;
			timeNextEvent[2] = simTime + exp(service.getMeanService());

			/* Actualizar la fila */
			queue.updateArrivalTimes();
		}
	}

	/*
	 * Generador de Reportes
	 */
	private void report() {
		System.out.println("Generando reportes...");
		System.out.println("Promedio de demora en la fila: " + totalDelays
				/ numCustDelayed);
		System.out.println("Promedio de clientes en la fila: " + areaNumInQueue
				/ simTime);
		System.out.println("Uso del servidor: " + areaServerStatus / simTime);
		System.out.println("Simulación terminó en: " + simTime);

	}

	/*
	 * Actualizar los acumuladores de área para las estadísticas de tiempos
	 * promedio
	 */
	private void updateTimeAvgStats() {
		System.out.println("Actualizando contadores estadísticos...");
		double timeSinceLastEvent = simTime - timeLastEvent;
		timeLastEvent = simTime;

		/* Actualizar contadores estadísticos */
		areaNumInQueue += queue.getNumInQueue() * timeSinceLastEvent;
		areaServerStatus += service.getServerStatus() * timeSinceLastEvent;
	}

	/*
	 * Generación de Variable Aleatoria usando una Distribución Exponencial
	 */
	private double exp(double mean) {
		// return -(1 / mean) * Math.log(Math.random());
		ExponentialDistribution ex = new ExponentialDistribution(mean);
		//return -(mean) * Math.log(Math.random());
		return ex.sample();
	}

	public int getNumDelaysRequired() {
		return numDelaysRequired;
	}

	public void setNumDelaysRequired(int numDelaysRequired) {
		this.numDelaysRequired = numDelaysRequired;
	}

	public int getNumEvents() {
		return numEvents;
	}

	public void setNumEvents(int numEvents) {
		this.numEvents = numEvents;
	}

	public double getMeanInterarrival() {
		return meanInterarrival;
	}

	public void setMeanInterarrival(double meanInterarrival) {
		this.meanInterarrival = meanInterarrival;
	}

	public double getMeanServiceB() {
		return meanServiceB;
	}

	public void setMeanServiceB(double meanServiceB) {
		this.meanServiceB = meanServiceB;
	}

	public int getNextEventType() {
		return nextEventType;
	}

	public void setNextEventType(int nextEventType) {
		this.nextEventType = nextEventType;
	}

	public int getNumCustDelayed() {
		return numCustDelayed;
	}

	public void setNumCustDelayed(int numCustDelayed) {
		this.numCustDelayed = numCustDelayed;
	}

}
